<template>
  <div
    class="list"
    ref="listRef"
    @dragstart="drag"
    @dragover="dragover"
    @dragend="dragend"
    @mouseout="out"
  >
    <div
      class="list-item"
      v-for="(item, index) in listData"
      :key="index"
      :orgIndex="index"
      :draggable="isDrag"
      @click="click"
    >
      <slot :row="item"></slot>
    </div>
  </div>
</template>
 
<script>
export default {
  name: 'DragList',
  props: {
    listData: Array, // list绑定的数据
    isDrag: Boolean, // 是否开启拖拽
    callDrop: Function,
    callClick: Function
  },
  data() {
    return {
      oldListInfo: [], // 拖拽前数据
      listInfo: [], // 拖拽后数据
      draging: null, // 被拖拽元素
      target: null, // 目标元素
      allow: false // 是否可拖拽，鼠标按下后才可拖拽
    };
  },
  created() {
    this.listInfo = this.listData;
  },
  methods: {
    click(e) {
      let listItem = this.getParentNode(e.target, 'list-item');
      if (!listItem) return;
      let clickItemIndex = this.getIndex(listItem);
      // call 自定义click，返回被点击数据
      if (this.callClick && typeof this.callClick == 'function') {
        this.callClick({
          index: clickItemIndex,
          item: this.listInfo[clickItemIndex]
        });
      }
    },
    drag(e) {
      this.draging = e.target;
      this.allow = true;
      this.oldListInfo = this.listInfo;
    },
    dragover(e) {
      e.preventDefault();
      if (!this.allow) return;
      let listItem = this.getParentNode(e.target, 'list-item');
      if (!listItem) return;

      this.target = listItem;

      let targetTop = this.target.getBoundingClientRect().top;
      let dragingTop = this.draging.getBoundingClientRect().top;

      let dragingIndex = this.getIndex(this.draging);
      let targetIndex = this.getIndex(this.target);

      if (
        this.target.className === 'list-item' &&
        this.target !== this.draging
      ) {
        if (this.target) {
          if (this.target.animated) {
            return;
          }
        }
        // 互换位置
        if (dragingIndex < targetIndex) {
          this.target.parentNode.insertBefore(
            this.draging,
            this.target.nextSibling
          );
        } else {
          this.target.parentNode.insertBefore(this.draging, this.target);
        }
        this.animate(targetTop, this.target);
        this.animate(dragingTop, this.draging);
      }
    },
    dragend() {
      // 返回调整后的数据，调用外部自定义函数
      this.listInfo = this.getNewData();

      if (this.callDrop && typeof this.callDrop == 'function') {
        this.callDrop({
          oldListInfo: this.oldListInfo,
          newListInfo: this.listInfo
        });
      }
    },
    // 鼠标移出list
    out(e) {
      if (
        e.clientY < this.$refs.listRef.getBoundingClientRect().top ||
        e.clientY > this.$refs.listRef.getBoundingClientRect().bottom
      ) {
        this.allow = false;
      }
    },
    /*
     * 按类名查询父元素
     * node：当前节点
     * pclassname：父元素类名
     */
    getParentNode(node, pclassname) {
      let p = node;
      if (p.className == pclassname) return p;
      while (p.parentNode) {
        if (p.parentNode.className == pclassname) {
          return p.parentNode;
        } else {
          p = p.parentNode;
        }
        if (p.nodeName == 'BODY') return 0;
      }
    },
    /*
     * 获取元素在父元素的索引
     * node：当前节点
     * pclassname：父元素类名
     */
    getIndex(el) {
      let domData = Array.from(this.$refs.listRef.childNodes);
      return domData.findIndex((i) => i.innerText == el.innerText);
    },
    // 动画效果
    animate(startPos, dom) {
      let offset = startPos - dom.getBoundingClientRect().top;
      dom.style.transition = 'none';
      dom.style.transform = `translateY(${offset}px)`;

      dom.offsetWidth;
      dom.style.transition = 'transform .3s';
      dom.style.transform = '';
      clearTimeout(dom.animated);

      dom.animated = setTimeout(() => {
        dom.style.transition = '';
        dom.style.transform = '';
        dom.animated = false;
      }, 300);
    },
    // 获取拖拽后的数据
    getNewData() {
      let currentNodes = Array.from(this.$refs.listRef.childNodes);
      let listArr = [];
      currentNodes.map((i, index) => {
        return (listArr[index] = this.listData[i.getAttribute('orgIndex')]);
      });
      return listArr;
    }
  }
};
</script>
 
<style scoped>
.list {
  display: block;
}
.list-item {
  width: 100%;
  height: 100%;
  user-select: none;
  cursor: pointer;
}
</style>